package ru.mcfr.oxygen.framework.extensions;

import ro.sync.contentcompletion.xml.ContextElement;
import ro.sync.contentcompletion.xml.WhatElementsCanGoHereContext;
import ro.sync.ecss.extensions.api.*;
import ro.sync.ecss.extensions.api.node.AuthorDocumentFragment;
import ro.sync.ecss.extensions.api.node.AuthorElement;
import ro.sync.ecss.extensions.api.node.AuthorNode;

import javax.swing.text.BadLocationException;
import java.util.List;

public class MOFSchemaAwareEditingHandler extends AuthorSchemaAwareEditingHandlerAdapter {

    private static final String SDF_NAMESPACE = "http://www.oxygenxml.com/sample/documentation";

    private static final String SDF_TABLE = "table";

    private static final String SDF_TABLE_ROW = "tr";

    private static final String SDF_TABLE_CELL = "td";

    private static final String SECTION = "section";

    protected static final String PARA = "para";

    protected static final String TITLE = "title";

    public boolean handleDelete(int offset, int deleteType, AuthorAccess authorAccess, boolean wordLevel)
            throws InvalidEditException {
        // Not handled.
        return false;
    }

    public boolean handleDeleteElementTags(AuthorNode nodeToUnwrap, AuthorAccess authorAccess)
            throws InvalidEditException {
        // Not handled.
        return false;
    }

    public boolean handleDeleteSelection(int selectionStart, int selectionEnd, int generatedByActionId,
                                         AuthorAccess authorAccess) throws InvalidEditException {
        // Not handled.
        return false;
    }

    public boolean handleJoinElements(AuthorNode targetNode, List<AuthorNode> nodesToJoin, AuthorAccess authorAccess)
            throws InvalidEditException {
        // Not handled.
        return false;
    }

    public boolean handlePasteFragment(int offset, AuthorDocumentFragment[] fragmentsToInsert, int actionId,
                                       AuthorAccess authorAccess) throws InvalidEditException {
        boolean handleInsertionEvent = false;
        AuthorSchemaManager authorSchemaManager = authorAccess.getDocumentController().getAuthorSchemaManager();
        if (!authorSchemaManager.isLearnSchema() &&
                !authorSchemaManager.hasLoadingErrors() &&
                authorSchemaManager.getAuthorSchemaAwareOptions().isEnableSmartPaste()) {
            handleInsertionEvent = handleInsertionEvent(offset, fragmentsToInsert, authorAccess);
        }
        return handleInsertionEvent;
    }

    public boolean handleTyping(int offset, char ch, AuthorAccess authorAccess)
            throws InvalidEditException {
        boolean handleTyping = false;
        AuthorSchemaManager authorSchemaManager = authorAccess.getDocumentController().getAuthorSchemaManager();
        if (!authorSchemaManager.isLearnSchema() &&
                !authorSchemaManager.hasLoadingErrors() &&
                authorSchemaManager.getAuthorSchemaAwareOptions().isEnableSmartTyping()) {
            try {
                AuthorDocumentFragment characterFragment =
                        authorAccess.getDocumentController().createNewDocumentTextFragment(String.valueOf(ch));
                handleTyping = handleInsertionEvent(offset, new AuthorDocumentFragment[]{characterFragment}, authorAccess);
            } catch (AuthorOperationException e) {
                throw new InvalidEditException(e.getMessage(), "Invalid typing event: " + e.getMessage(), e, false);
            }
        }
        return handleTyping;
    }

    private boolean handleInsertionEvent(
            int offset,
            AuthorDocumentFragment[] fragmentsToInsert,
            AuthorAccess authorAccess) throws InvalidEditException {
        AuthorSchemaManager authorSchemaManager = authorAccess.getDocumentController().getAuthorSchemaManager();
        boolean handleEvent = false;
        try {
            AuthorNode nodeAtInsertionOffset = authorAccess.getDocumentController().getNodeAtOffset(offset);
            if (isElementWithNameAndNamespace(nodeAtInsertionOffset, SDF_TABLE)) {
                // Check if the fragment is allowed as it is.
                boolean canInsertFragments = authorSchemaManager.canInsertDocumentFragments(
                        fragmentsToInsert,
                        offset,
                        AuthorSchemaManager.VALIDATION_MODE_STRICT_FIRST_CHILD_LAX_OTHERS);
                if (!canInsertFragments) {
                    handleEvent = handleInvalidInsertionEventInTable(
                            offset,
                            fragmentsToInsert,
                            authorAccess,
                            authorSchemaManager);
                }
            } else if (isElementWithNameAndNamespace(nodeAtInsertionOffset, SECTION)) {
                // Check if the fragment is allowed as it is.
                boolean canInsertFragments = authorSchemaManager.canInsertDocumentFragments(
                        fragmentsToInsert,
                        offset,
                        AuthorSchemaManager.VALIDATION_MODE_STRICT_FIRST_CHILD_LAX_OTHERS);
                if (!canInsertFragments) {
                    // Insertion in 'section' element
                    handleEvent = handleInvalidInsertionEventInSect(
                            offset,
                            fragmentsToInsert,
                            authorAccess,
                            authorSchemaManager);
                }
            }
        } catch (BadLocationException e) {
            throw new InvalidEditException(e.getMessage(), "Invalid typing event: " + e.getMessage(), e, false);
        } catch (AuthorOperationException e) {
            throw new InvalidEditException(e.getMessage(), "Invalid typing event: " + e.getMessage(), e, false);
        }
        return handleEvent;
    }

    protected boolean isElementWithNameAndNamespace(AuthorNode node, String elementLocalName) {
        boolean result = false;
        if (node.getType() == AuthorNode.NODE_TYPE_ELEMENT) {
            AuthorElement element = (AuthorElement) node;
            result = elementLocalName.equals(element.getLocalName()) && element.getNamespace().equals(SDF_NAMESPACE);
        }
        return result;
    }

    private boolean handleInvalidInsertionEventInTable(
            int offset,
            AuthorDocumentFragment[] fragmentsToInsert,
            AuthorAccess authorAccess,
            AuthorSchemaManager authorSchemaManager) throws BadLocationException, AuthorOperationException {
        boolean handleEvent = false;
        // Typing/paste inside a SDF table. We will try to wrap the fragment into a new cell and insert it inside a new row.
        WhatElementsCanGoHereContext context = authorSchemaManager.createWhatElementsCanGoHereContext(offset);
        StringBuilder xmlFragment = new StringBuilder("<");
        xmlFragment.append(SDF_TABLE_ROW);
        if (SDF_NAMESPACE != null && SDF_NAMESPACE.length() != 0) {
            xmlFragment.append(" xmlns=\"").append(SDF_NAMESPACE).append("\"");
        }
        xmlFragment.append("/>");

        // Check if a row can be inserted at the current offset.
        boolean canInsertRow = authorSchemaManager.canInsertDocumentFragments(
                new AuthorDocumentFragment[]{authorAccess.getDocumentController().createNewDocumentFragmentInContext(xmlFragment.toString(), offset)},
                context,
                AuthorSchemaManager.VALIDATION_MODE_STRICT_FIRST_CHILD_LAX_OTHERS);

        // Derive the context by adding a new row element with a cell.
        if (canInsertRow) {
            pushContextElement(context, SDF_TABLE_ROW);
            pushContextElement(context, SDF_TABLE_CELL);

            // Test if fragments can be inserted in the new context.
            if (authorSchemaManager.canInsertDocumentFragments(
                    fragmentsToInsert,
                    context,
                    AuthorSchemaManager.VALIDATION_MODE_STRICT_FIRST_CHILD_LAX_OTHERS)) {

                // Insert a new row with a cell.
                xmlFragment = new StringBuilder("<");
                xmlFragment.append(SDF_TABLE_ROW);

                if (SDF_NAMESPACE != null && SDF_NAMESPACE.length() != 0) {
                    xmlFragment.append(" xmlns=\"").append(SDF_NAMESPACE).append("\"");
                }
                xmlFragment.append("><");
                xmlFragment.append(SDF_TABLE_CELL);
                xmlFragment.append("/></");
                xmlFragment.append(SDF_TABLE_ROW);
                xmlFragment.append(">");
                authorAccess.getDocumentController().insertXMLFragment(xmlFragment.toString(), offset);

                // Get the newly inserted cell.
                AuthorNode newCell = authorAccess.getDocumentController().getNodeAtOffset(offset + 2);
                for (int i = 0; i < fragmentsToInsert.length; i++) {
                    authorAccess.getDocumentController().insertFragment(newCell.getEndOffset(), fragmentsToInsert[i]);
                }

                handleEvent = true;
            }
        }
        return handleEvent;
    }

    protected void pushContextElement(WhatElementsCanGoHereContext context, String elementName) {
        ContextElement contextElement = new ContextElement();
        contextElement.setQName(elementName);
        contextElement.setNamespace(SDF_NAMESPACE);
        context.pushContextElement(contextElement, null);
    }

    private boolean handleInvalidInsertionEventInSect(int offset, AuthorDocumentFragment[] fragmentsToInsert, AuthorAccess authorAccess,
                                                      AuthorSchemaManager authorSchemaManager) throws BadLocationException, AuthorOperationException {
        boolean handleEvent = false;
        // Typing/paste inside an section.

        AuthorElement sectionElement = (AuthorElement) authorAccess.getDocumentController().getNodeAtOffset(offset);

        if (sectionElement.getStartOffset() + 1 == sectionElement.getEndOffset()) {
            // Empty section element
            WhatElementsCanGoHereContext context = authorSchemaManager.createWhatElementsCanGoHereContext(offset);
            // Derive the context by adding a title.
            pushContextElement(context, TITLE);

            // Test if fragments can be inserted in 'title' element
            if (authorSchemaManager.canInsertDocumentFragments(
                    fragmentsToInsert,
                    context,
                    AuthorSchemaManager.VALIDATION_MODE_STRICT_FIRST_CHILD_LAX_OTHERS)) {
                // Create a title structure and insert fragments inside
                StringBuilder xmlFragment = new StringBuilder("<").append(TITLE);
                if (SDF_NAMESPACE != null && SDF_NAMESPACE.length() != 0) {
                    xmlFragment.append(" xmlns=\"").append(SDF_NAMESPACE).append("\"");
                }
                xmlFragment.append(">").append("</").append(TITLE).append(">");
                // Insert title
                authorAccess.getDocumentController().insertXMLFragment(xmlFragment.toString(), offset);

                // Insert fragments
                AuthorNode newParaNode = authorAccess.getDocumentController().getNodeAtOffset(offset + 1);
                for (int i = 0; i < fragmentsToInsert.length; i++) {
                    authorAccess.getDocumentController().insertFragment(newParaNode.getEndOffset(), fragmentsToInsert[i]);
                }
                handleEvent = true;
            }
        } else {
            // Check if there is just a title.
            List<AuthorNode> contentNodes = sectionElement.getContentNodes();
            if (contentNodes.size() == 1) {
                AuthorNode child = contentNodes.get(0);
                boolean isTitleChild = isElementWithNameAndNamespace(child, TITLE);
                if (isTitleChild && child.getEndOffset() < offset) {
                    // We are after the title.

                    // Empty sect element
                    WhatElementsCanGoHereContext context = authorSchemaManager.createWhatElementsCanGoHereContext(offset);
                    // Derive the context by adding a para
                    pushContextElement(context, PARA);

                    // Test if fragments can be inserted in 'para' element
                    if (authorSchemaManager.canInsertDocumentFragments(
                            fragmentsToInsert,
                            context,
                            AuthorSchemaManager.VALIDATION_MODE_STRICT_FIRST_CHILD_LAX_OTHERS)) {
                        // Create a para structure and insert fragments inside
                        StringBuilder xmlFragment = new StringBuilder("<").append(PARA);
                        if (SDF_NAMESPACE != null && SDF_NAMESPACE.length() != 0) {
                            xmlFragment.append(" xmlns=\"").append(SDF_NAMESPACE).append("\"");
                        }
                        xmlFragment.append(">").append("</").append(PARA).append(">");
                        // Insert para
                        authorAccess.getDocumentController().insertXMLFragment(xmlFragment.toString(), offset);

                        // Insert fragments
                        AuthorNode newParaNode = authorAccess.getDocumentController().getNodeAtOffset(offset + 1);
                        for (int i = 0; i < fragmentsToInsert.length; i++) {
                            authorAccess.getDocumentController().insertFragment(newParaNode.getEndOffset(), fragmentsToInsert[i]);
                        }
                        handleEvent = true;
                    }
                }
            }
        }
        return handleEvent;
    }
}
